//
// Created by Administrator on 2021/11/24.
//

#include "Application.h"
#include "ShaderProgram.h"
#include "Camera.h"
#include "MeshUtility.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

class App : public Application
{
    enum class Entity
    {
        FourPoint,
        Sphere,
        Normal,
        Count
    };

    enum class UBO
    {
        PerDraw,
        Count
    };

    struct PerDraw
    {
        Mat4x4f viewMatrix;
        Mat4x4f projMatrix;
        float time;
    };

    struct PerObject
    {
        Mat4x4f modelMatrix;
    };

public:
    App() : Application("GeoShader", 800, 600) {}

protected:
    void OnCreate() override
    {
        SetupGL();
        SetupCamera();
        SetupUBO();
        SetupFourPoint();
        SetupSphere();
        SetupNormal();
    }

    void OnUpdate() override
    {
        UpdatePerDrawUBO();
        UpdatePerObjectUBO();
    }

    void OnRender() override
    {
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        RenderFourPoint();
        RenderSphere();
        RenderNormal();
    }

    void OnKey(int keyCode, int action, int mods) override
    {
        switch (keyCode)
        {
            case GLFW_KEY_A:
                m_camera.transform().position().x() -= 0.1f;
                break;
            case GLFW_KEY_W:
                m_camera.transform().position().z() -= 0.1f;
                break;
            case GLFW_KEY_D:
                m_camera.transform().position().x() += 0.1f;
                break;
            case GLFW_KEY_S:
                m_camera.transform().position().z() += 0.1f;
                break;
            case GLFW_KEY_Q:
                m_camera.transform().rotation().y() -= 1.0f;
                break;
            case GLFW_KEY_E:
                m_camera.transform().rotation().y() -= 1.0f;
                break;
        }
    }

    void OnDestroy() override
    {
        glDeleteVertexArrays(int(Entity::Count), m_vao);
        glDeleteBuffers(int(Entity::Count), m_vbo);
        glDeleteBuffers(int(Entity::Count), m_ebo);
        glDeleteTextures(int(Entity::Count), m_tex2D);
        glDeleteBuffers(int(Entity::Count), m_objectUBO);
        glDeleteBuffers(int(UBO::Count), m_perUBO);
    }

private:
    void SetupGL()
    {
        glEnable(GL_MULTISAMPLE);
        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glEnable(GL_DEPTH_TEST);

        glCreateVertexArrays(int(Entity::Count), m_vao);
        glCreateBuffers(int(Entity::Count), m_vbo);
        glCreateBuffers(int(Entity::Count), m_ebo);
        glCreateBuffers(int(Entity::Count), m_objectUBO);
        glCreateTextures(GL_TEXTURE_2D, int(Entity::Count), m_tex2D);
        glCreateBuffers(int(UBO::Count), m_perUBO);
    }

    void SetupCamera()
    {
        m_camera.Setup(800.0f / 600.0f, 60.0f, 0.1f, 100.0f);
        m_camera.transform().position() = Vec3f(0.0f, 0.0f, 10.0f);
    }

    void SetupUBO()
    {
        glNamedBufferData(m_perUBO[int(UBO::PerDraw)], sizeof(PerDraw), nullptr, GL_STATIC_DRAW);
        glBindBufferBase(GL_UNIFORM_BUFFER, 0, m_perUBO[int(UBO::PerDraw)]);
        glNamedBufferData(m_objectUBO[int(Entity::FourPoint)], 4 * sizeof(PerObject), nullptr, GL_STATIC_DRAW);
        glBindBufferBase(GL_UNIFORM_BUFFER, 1, m_objectUBO[int(Entity::FourPoint)]);
        glNamedBufferData(m_objectUBO[int(Entity::Sphere)], sizeof(PerObject), nullptr, GL_STATIC_DRAW);
        glBindBufferBase(GL_UNIFORM_BUFFER, 2, m_objectUBO[int(Entity::Sphere)]);
    }

    void SetupFourPoint()
    {
        m_sp[int(Entity::FourPoint)] = ShaderProgram("../../GeoShader/shaders/point_vs.glsl",
                                                     "../../GeoShader/shaders/point_fs.glsl",
                                                     "",
                                                     "",
                                                     "../../GeoShader/shaders/point_gs.glsl");
        static Vec3f positions = Vec3f(0.0f, 0.0f, 0.0f);

        glBindVertexArray(m_vao[int(Entity::FourPoint)]);

        glBindBuffer(GL_ARRAY_BUFFER, m_vbo[int(Entity::FourPoint)]);
        glBufferData(GL_ARRAY_BUFFER, sizeof(positions), &positions, GL_STATIC_DRAW);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
        glEnableVertexAttribArray(0);
    }

    void SetupSphere()
    {
        Vertex* vertices;
        GLuint* indices;
        int vertexCount;
        MeshUtility::GenerateSphere(40, 2.0f, vertices, indices, vertexCount, m_indexCount[int(Entity::Sphere)]);
        m_sp[int(Entity::Sphere)] = ShaderProgram("../../GeoShader/shaders/sphere_vs.glsl",
                                                  "../../GeoShader/shaders/sphere_fs.glsl",
                                                  "",
                                                  "",
                                                  "../../GeoShader/shaders/sphere_gs.glsl");
        int width, height, channels;
        stbi_set_flip_vertically_on_load(true);
        auto* data = stbi_load("../../GeoShader/textures/sphere.png", &width, &height, &channels, 0);
        glBindTexture(GL_TEXTURE_2D, m_tex2D[int(Entity::Sphere)]);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
        stbi_image_free(data);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        glBindVertexArray(m_vao[int(Entity::Sphere)]);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo[int(Entity::Sphere)]);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexCount[int(Entity::Sphere)] * sizeof(GLuint), indices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, m_vbo[int(Entity::Sphere)]);
        glBufferData(GL_ARRAY_BUFFER, vertexCount * sizeof(Vertex), vertices, GL_STATIC_DRAW);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(sizeof(Vec3f)));
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(2 * sizeof(Vec3f)));
        glEnableVertexAttribArray(2);
    }

    void SetupNormal()
    {
        m_sp[int(Entity::Normal)] = ShaderProgram("../../GeoShader/shaders/normal_vs.glsl",
                                                  "../../GeoShader/shaders/normal_fs.glsl",
                                                  "",
                                                  "",
                                                  "../../GeoShader/shaders/normal_gs.glsl");
    }

    void UpdatePerDrawUBO()
    {
        auto* ptr = static_cast<PerDraw*>(glMapNamedBuffer(m_perUBO[int(UBO::PerDraw)], GL_WRITE_ONLY));
        ptr->viewMatrix = m_camera.worldToCameraMatrix().Transposed();
        ptr->projMatrix = m_camera.projectionMatrix().Transposed();
        ptr->time = glfwGetTime();
        glUnmapNamedBuffer(m_perUBO[int(UBO::PerDraw)]);
    }

    void UpdatePerObjectUBO()
    {
        auto* ptr = static_cast<PerObject*>(glMapNamedBuffer(m_objectUBO[int(Entity::FourPoint)], GL_WRITE_ONLY));
        ptr->modelMatrix = Mat4x4f(1.0f, 0.0f, 0.0f, -3.0f,
                                   0.0f, 1.0f, 0.0f, 3.0f,
                                   0.0f, 0.0f, 1.0f, 0.0f,
                                   0.0f, 0.0f, 0.0f, 1.0f).Transposed();
        ++ptr;
        ptr->modelMatrix = Mat4x4f(1.0f, 0.0f, 0.0f, -3.0f,
                                   0.0f, 1.0f, 0.0f, -3.0f,
                                   0.0f, 0.0f, 1.0f, 0.0f,
                                   0.0f, 0.0f, 0.0f, 1.0f).Transposed();
        ++ptr;
        ptr->modelMatrix = Mat4x4f(1.0f, 0.0f, 0.0f, 3.0f,
                                   0.0f, 1.0f, 0.0f, -3.0f,
                                   0.0f, 0.0f, 1.0f, 0.0f,
                                   0.0f, 0.0f, 0.0f, 1.0f).Transposed();
        ++ptr;
        ptr->modelMatrix = Mat4x4f(1.0f, 0.0f, 0.0f, 3.0f,
                                   0.0f, 1.0f, 0.0f, 3.0f,
                                   0.0f, 0.0f, 1.0f, 0.0f,
                                   0.0f, 0.0f, 0.0f, 1.0f).Transposed();
        glUnmapNamedBuffer(m_objectUBO[int(Entity::FourPoint)]);

        ptr = static_cast<PerObject*>(glMapNamedBuffer(m_objectUBO[int(Entity::Sphere)], GL_WRITE_ONLY));
        ptr->modelMatrix = Mat4x4f::Identity().Transposed();
        glUnmapNamedBuffer(m_objectUBO[int(Entity::Sphere)]);
    }

    void RenderFourPoint()
    {
        m_sp[int(Entity::FourPoint)].Use();

        glBindVertexArray(m_vao[int(Entity::FourPoint)]);
        glDrawArraysInstanced(GL_POINTS, 0, 1, 4);
    }

    void RenderSphere()
    {
        m_sp[int(Entity::Sphere)].Use();

        glBindVertexArray(m_vao[int(Entity::Sphere)]);
        glDrawElements(GL_TRIANGLES, m_indexCount[int(Entity::Sphere)], GL_UNSIGNED_INT, nullptr);
    }

    void RenderNormal()
    {
        m_sp[int(Entity::Normal)].Use();

        glBindVertexArray(m_vao[int(Entity::Sphere)]);
        glDrawElements(GL_TRIANGLES, m_indexCount[int(Entity::Sphere)], GL_UNSIGNED_INT, nullptr);
    }

    GLuint m_vao[int(Entity::Count)];
    GLuint m_vbo[int(Entity::Count)];
    GLuint m_ebo[int(Entity::Count)];
    GLuint m_tex2D[int(Entity::Count)];
    int m_indexCount[int(Entity::Count)];
    GLuint m_objectUBO[int(Entity::Count)];
    ShaderProgram m_sp[int(Entity::Count)];
    GLuint m_perUBO[int(UBO::Count)];
    Camera m_camera;

};

int main()
{
    App().Run();
    return 0;
}